// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/service_worker/service_worker_registration.h"

#include <vector>

#include "base/threading/thread_task_runner_handle.h"
#include "content/browser/service_worker/embedded_worker_status.h"
#include "content/browser/service_worker/service_worker_context_core.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h"
#include "content/browser/service_worker/service_worker_info.h"
#include "content/browser/service_worker/service_worker_metrics.h"
#include "content/browser/service_worker/service_worker_register_job.h"
#include "content/common/service_worker/service_worker_messages.h"
#include "content/common/service_worker/service_worker_utils.h"
#include "content/public/browser/browser_thread.h"

namespace content {

namespace {

    ServiceWorkerVersionInfo GetVersionInfo(ServiceWorkerVersion* version)
    {
        if (!version)
            return ServiceWorkerVersionInfo();
        return version->GetInfo();
    }

} // namespace

ServiceWorkerRegistration::ServiceWorkerRegistration(
    const GURL& pattern,
    int64_t registration_id,
    base::WeakPtr<ServiceWorkerContextCore> context)
    : pattern_(pattern)
    , registration_id_(registration_id)
    , is_deleted_(false)
    , is_uninstalling_(false)
    , is_uninstalled_(false)
    , should_activate_when_ready_(false)
    , resources_total_size_bytes_(0)
    , context_(context)
    , task_runner_(base::ThreadTaskRunnerHandle::Get())
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    DCHECK_NE(kInvalidServiceWorkerRegistrationId, registration_id);
    DCHECK(context_);
    context_->AddLiveRegistration(this);
}

ServiceWorkerRegistration::~ServiceWorkerRegistration()
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    DCHECK(!listeners_.might_have_observers());
    if (context_)
        context_->RemoveLiveRegistration(registration_id_);
    if (active_version())
        active_version()->RemoveListener(this);
}

ServiceWorkerVersion* ServiceWorkerRegistration::GetNewestVersion() const
{
    if (installing_version())
        return installing_version();
    if (waiting_version())
        return waiting_version();
    return active_version();
}

void ServiceWorkerRegistration::AddListener(Listener* listener)
{
    listeners_.AddObserver(listener);
}

void ServiceWorkerRegistration::RemoveListener(Listener* listener)
{
    listeners_.RemoveObserver(listener);
}

void ServiceWorkerRegistration::NotifyRegistrationFailed()
{
    for (auto& observer : listeners_)
        observer.OnRegistrationFailed(this);
    NotifyRegistrationFinished();
}

void ServiceWorkerRegistration::NotifyUpdateFound()
{
    for (auto& observer : listeners_)
        observer.OnUpdateFound(this);
}

void ServiceWorkerRegistration::NotifyVersionAttributesChanged(
    ChangedVersionAttributesMask mask)
{
    for (auto& observer : listeners_)
        observer.OnVersionAttributesChanged(this, mask, GetInfo());
    if (mask.active_changed() || mask.waiting_changed())
        NotifyRegistrationFinished();
}

ServiceWorkerRegistrationInfo ServiceWorkerRegistration::GetInfo()
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    return ServiceWorkerRegistrationInfo(
        pattern(), registration_id_,
        is_deleted_ ? ServiceWorkerRegistrationInfo::IS_DELETED
                    : ServiceWorkerRegistrationInfo::IS_NOT_DELETED,
        GetVersionInfo(active_version_.get()),
        GetVersionInfo(waiting_version_.get()),
        GetVersionInfo(installing_version_.get()), resources_total_size_bytes_);
}

void ServiceWorkerRegistration::SetActiveVersion(
    const scoped_refptr<ServiceWorkerVersion>& version)
{
    if (active_version_ == version)
        return;

    should_activate_when_ready_ = false;

    ChangedVersionAttributesMask mask;
    if (version)
        UnsetVersionInternal(version.get(), &mask);
    if (active_version_)
        active_version_->RemoveListener(this);
    active_version_ = version;
    if (active_version_) {
        active_version_->AddListener(this);
        active_version_->SetNavigationPreloadState(navigation_preload_state_);
    }
    mask.add(ChangedVersionAttributesMask::ACTIVE_VERSION);

    NotifyVersionAttributesChanged(mask);
}

void ServiceWorkerRegistration::SetWaitingVersion(
    const scoped_refptr<ServiceWorkerVersion>& version)
{
    if (waiting_version_ == version)
        return;

    should_activate_when_ready_ = false;

    ChangedVersionAttributesMask mask;
    if (version)
        UnsetVersionInternal(version.get(), &mask);
    waiting_version_ = version;
    mask.add(ChangedVersionAttributesMask::WAITING_VERSION);

    NotifyVersionAttributesChanged(mask);
}

void ServiceWorkerRegistration::SetInstallingVersion(
    const scoped_refptr<ServiceWorkerVersion>& version)
{
    if (installing_version_ == version)
        return;

    ChangedVersionAttributesMask mask;
    if (version)
        UnsetVersionInternal(version.get(), &mask);
    installing_version_ = version;
    mask.add(ChangedVersionAttributesMask::INSTALLING_VERSION);

    NotifyVersionAttributesChanged(mask);
}

void ServiceWorkerRegistration::UnsetVersion(ServiceWorkerVersion* version)
{
    if (!version)
        return;
    ChangedVersionAttributesMask mask;
    UnsetVersionInternal(version, &mask);
    if (mask.changed())
        NotifyVersionAttributesChanged(mask);
}

void ServiceWorkerRegistration::UnsetVersionInternal(
    ServiceWorkerVersion* version,
    ChangedVersionAttributesMask* mask)
{
    DCHECK(version);
    if (installing_version_.get() == version) {
        installing_version_ = NULL;
        mask->add(ChangedVersionAttributesMask::INSTALLING_VERSION);
    } else if (waiting_version_.get() == version) {
        waiting_version_ = NULL;
        should_activate_when_ready_ = false;
        mask->add(ChangedVersionAttributesMask::WAITING_VERSION);
    } else if (active_version_.get() == version) {
        active_version_->RemoveListener(this);
        active_version_ = NULL;
        mask->add(ChangedVersionAttributesMask::ACTIVE_VERSION);
    }
}

void ServiceWorkerRegistration::ActivateWaitingVersionWhenReady()
{
    DCHECK(waiting_version());
    should_activate_when_ready_ = true;
    if (IsReadyToActivate())
        ActivateWaitingVersion(false /* delay */);
}

void ServiceWorkerRegistration::ClaimClients()
{
    DCHECK(context_);
    DCHECK(active_version());

    for (std::unique_ptr<ServiceWorkerContextCore::ProviderHostIterator> it = context_->GetProviderHostIterator();
         !it->IsAtEnd(); it->Advance()) {
        ServiceWorkerProviderHost* host = it->GetProviderHost();
        if (host->IsHostToRunningServiceWorker())
            continue;
        if (host->controlling_version() == active_version())
            continue;
        if (!host->IsContextSecureForServiceWorker())
            continue;
        if (host->MatchRegistration() == this)
            host->ClaimedByRegistration(this);
    }
}

void ServiceWorkerRegistration::ClearWhenReady()
{
    DCHECK(context_);
    if (is_uninstalling_)
        return;
    is_uninstalling_ = true;

    context_->storage()->NotifyUninstallingRegistration(this);
    context_->storage()->DeleteRegistration(
        id(),
        pattern().GetOrigin(),
        base::Bind(&ServiceWorkerUtils::NoOpStatusCallback));

    if (!active_version() || !active_version()->HasControllee())
        Clear();
}

void ServiceWorkerRegistration::AbortPendingClear(
    const StatusCallback& callback)
{
    DCHECK(context_);
    if (!is_uninstalling()) {
        callback.Run(SERVICE_WORKER_OK);
        return;
    }
    is_uninstalling_ = false;
    context_->storage()->NotifyDoneUninstallingRegistration(this);

    scoped_refptr<ServiceWorkerVersion> most_recent_version = waiting_version() ? waiting_version() : active_version();
    DCHECK(most_recent_version.get());
    context_->storage()->NotifyInstallingRegistration(this);
    context_->storage()->StoreRegistration(
        this,
        most_recent_version.get(),
        base::Bind(&ServiceWorkerRegistration::OnRestoreFinished,
            this,
            callback,
            most_recent_version));
}

void ServiceWorkerRegistration::OnNoControllees(ServiceWorkerVersion* version)
{
    if (!context_)
        return;
    DCHECK_EQ(active_version(), version);
    if (is_uninstalling_)
        Clear();
    else if (IsReadyToActivate())
        ActivateWaitingVersion(true /* delay */);
}

void ServiceWorkerRegistration::OnNoWork(ServiceWorkerVersion* version)
{
    if (!context_)
        return;
    DCHECK_EQ(active_version(), version);
    if (IsReadyToActivate())
        ActivateWaitingVersion(false /* delay */);
}

bool ServiceWorkerRegistration::IsReadyToActivate() const
{
    if (!should_activate_when_ready_)
        return false;

    DCHECK(waiting_version());
    const ServiceWorkerVersion* active = active_version();
    if (!active)
        return true;
    if (!active->HasWork() && (!active->HasControllee() || waiting_version()->skip_waiting())) {
        return true;
    }
    return false;
}

void ServiceWorkerRegistration::ActivateWaitingVersion(bool delay)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    DCHECK(context_);
    DCHECK(IsReadyToActivate());
    should_activate_when_ready_ = false;
    scoped_refptr<ServiceWorkerVersion> activating_version = waiting_version();
    scoped_refptr<ServiceWorkerVersion> exiting_version = active_version();

    if (activating_version->is_redundant())
        return; // Activation is no longer relevant.

    // "5. If exitingWorker is not null,
    if (exiting_version.get()) {
        // TODO(falken): Update the quoted spec comments once
        // https://github.com/slightlyoff/ServiceWorker/issues/916 is codified in
        // the spec.
        // "1. Wait for exitingWorker to finish handling any in-progress requests."
        // This is already handled by IsReadyToActivate().
        // "2. Terminate exitingWorker."
        exiting_version->StopWorker(
            base::Bind(&ServiceWorkerUtils::NoOpStatusCallback));
        // "3. Run the [[UpdateState]] algorithm passing exitingWorker and
        // "redundant" as the arguments."
        exiting_version->SetStatus(ServiceWorkerVersion::REDUNDANT);
    }

    // "6. Set serviceWorkerRegistration.activeWorker to activatingWorker."
    // "7. Set serviceWorkerRegistration.waitingWorker to null."
    SetActiveVersion(activating_version);

    // "8. Run the [[UpdateState]] algorithm passing registration.activeWorker and
    // "activating" as arguments."
    activating_version->SetStatus(ServiceWorkerVersion::ACTIVATING);
    // "9. Fire a simple event named controllerchange..."
    if (activating_version->skip_waiting()) {
        for (auto& observer : listeners_)
            observer.OnSkippedWaiting(this);
    }

    // "10. Queue a task to fire an event named activate..."
    // The browser could be shutting down. To avoid spurious start worker
    // failures, wait a bit before continuing.
    if (delay) {
        task_runner_->PostDelayedTask(
            FROM_HERE, base::Bind(&ServiceWorkerRegistration::ContinueActivation, this, activating_version),
            base::TimeDelta::FromSeconds(1));
    } else {
        ContinueActivation(std::move(activating_version));
    }
}

void ServiceWorkerRegistration::ContinueActivation(
    scoped_refptr<ServiceWorkerVersion> activating_version)
{
    if (!context_)
        return;
    if (active_version() != activating_version.get())
        return;
    DCHECK_EQ(ServiceWorkerVersion::ACTIVATING, activating_version->status());
    activating_version->RunAfterStartWorker(
        ServiceWorkerMetrics::EventType::ACTIVATE,
        base::Bind(&ServiceWorkerRegistration::DispatchActivateEvent, this,
            activating_version),
        base::Bind(&ServiceWorkerRegistration::OnActivateEventFinished, this,
            activating_version));
}

void ServiceWorkerRegistration::DeleteVersion(
    const scoped_refptr<ServiceWorkerVersion>& version)
{
    DCHECK_EQ(id(), version->registration_id());

    UnsetVersion(version.get());

    for (std::unique_ptr<ServiceWorkerContextCore::ProviderHostIterator> it = context_->GetProviderHostIterator();
         !it->IsAtEnd(); it->Advance()) {
        ServiceWorkerProviderHost* host = it->GetProviderHost();
        if (host->controlling_version() == version)
            host->NotifyControllerLost();
    }

    version->Doom();

    if (!active_version() && !waiting_version()) {
        // Delete the records from the db.
        context_->storage()->DeleteRegistration(
            id(), pattern().GetOrigin(),
            base::Bind(&ServiceWorkerRegistration::OnDeleteFinished, this));
        // But not from memory if there is a version in the pipeline.
        // TODO(falken): Fix this logic. There could be a running register job for
        // this registration that hasn't set installing_version() yet.
        if (installing_version()) {
            is_deleted_ = false;
        } else {
            is_uninstalled_ = true;
            NotifyRegistrationFailed();
        }
    }
}

void ServiceWorkerRegistration::NotifyRegistrationFinished()
{
    std::vector<base::Closure> callbacks;
    callbacks.swap(registration_finished_callbacks_);
    for (const auto& callback : callbacks)
        callback.Run();
}

void ServiceWorkerRegistration::SetTaskRunnerForTest(
    scoped_refptr<base::SingleThreadTaskRunner> task_runner)
{
    task_runner_ = task_runner;
}

void ServiceWorkerRegistration::EnableNavigationPreload(bool enable)
{
    navigation_preload_state_.enabled = enable;
    if (active_version_)
        active_version_->SetNavigationPreloadState(navigation_preload_state_);
}

void ServiceWorkerRegistration::SetNavigationPreloadHeader(
    const std::string& header)
{
    navigation_preload_state_.header = header;
    if (active_version_)
        active_version_->SetNavigationPreloadState(navigation_preload_state_);
}

void ServiceWorkerRegistration::RegisterRegistrationFinishedCallback(
    const base::Closure& callback)
{
    // This should only be called if the registration is in progress.
    DCHECK(!active_version() && !waiting_version() && !is_uninstalled() && !is_uninstalling());
    registration_finished_callbacks_.push_back(callback);
}

void ServiceWorkerRegistration::DispatchActivateEvent(
    scoped_refptr<ServiceWorkerVersion> activating_version)
{
    if (activating_version != active_version()) {
        OnActivateEventFinished(activating_version, SERVICE_WORKER_ERROR_FAILED);
        return;
    }

    DCHECK_EQ(ServiceWorkerVersion::ACTIVATING, activating_version->status());
    DCHECK_EQ(EmbeddedWorkerStatus::RUNNING, activating_version->running_status())
        << "Worker stopped too soon after it was started.";
    int request_id = activating_version->StartRequest(
        ServiceWorkerMetrics::EventType::ACTIVATE,
        base::Bind(&ServiceWorkerRegistration::OnActivateEventFinished, this,
            activating_version));
    activating_version
        ->DispatchSimpleEvent<ServiceWorkerHostMsg_ActivateEventFinished>(
            request_id, ServiceWorkerMsg_ActivateEvent(request_id));
}

void ServiceWorkerRegistration::OnActivateEventFinished(
    scoped_refptr<ServiceWorkerVersion> activating_version,
    ServiceWorkerStatusCode status)
{
    // Activate is prone to failing due to shutdown, because it's triggered when
    // tabs close.
    bool is_shutdown = !context_ || context_->wrapper()->process_manager()->IsShutdown();
    ServiceWorkerMetrics::RecordActivateEventStatus(status, is_shutdown);

    if (!context_ || activating_version != active_version() || activating_version->status() != ServiceWorkerVersion::ACTIVATING) {
        return;
    }

    // Normally, the worker is committed to become activated once we get here, per
    // spec. E.g., if the script rejected waitUntil or had an unhandled exception,
    // it should still be activated. However, if the failure occurred during
    // shutdown, ignore it to give the worker another chance the next time the
    // browser starts up.
    if (is_shutdown && status != SERVICE_WORKER_OK)
        return;

    // "Run the Update State algorithm passing registration's active worker and
    // 'activated' as the arguments."
    activating_version->SetStatus(ServiceWorkerVersion::ACTIVATED);
    context_->storage()->UpdateToActiveState(
        this, base::Bind(&ServiceWorkerUtils::NoOpStatusCallback));
}

void ServiceWorkerRegistration::OnDeleteFinished(
    ServiceWorkerStatusCode status)
{
    // Intentionally empty completion callback, used to prevent
    // |this| from being deleted until the storage method completes.
}

void ServiceWorkerRegistration::Clear()
{
    is_uninstalling_ = false;
    is_uninstalled_ = true;
    should_activate_when_ready_ = false;
    if (context_)
        context_->storage()->NotifyDoneUninstallingRegistration(this);

    std::vector<scoped_refptr<ServiceWorkerVersion>> versions_to_doom;
    ChangedVersionAttributesMask mask;
    if (installing_version_.get()) {
        versions_to_doom.push_back(installing_version_);
        installing_version_ = nullptr;
        mask.add(ChangedVersionAttributesMask::INSTALLING_VERSION);
    }
    if (waiting_version_.get()) {
        versions_to_doom.push_back(waiting_version_);
        waiting_version_ = nullptr;
        mask.add(ChangedVersionAttributesMask::WAITING_VERSION);
    }
    if (active_version_.get()) {
        versions_to_doom.push_back(active_version_);
        active_version_->RemoveListener(this);
        active_version_ = nullptr;
        mask.add(ChangedVersionAttributesMask::ACTIVE_VERSION);
    }

    if (mask.changed()) {
        NotifyVersionAttributesChanged(mask);

        // Doom only after notifying attributes changed, because the spec requires
        // the attributes to be cleared by the time the statechange event is
        // dispatched.
        for (const auto& version : versions_to_doom)
            version->Doom();
    }

    for (auto& observer : listeners_)
        observer.OnRegistrationFinishedUninstalling(this);
}

void ServiceWorkerRegistration::OnRestoreFinished(
    const StatusCallback& callback,
    scoped_refptr<ServiceWorkerVersion> version,
    ServiceWorkerStatusCode status)
{
    if (!context_) {
        callback.Run(SERVICE_WORKER_ERROR_ABORT);
        return;
    }
    context_->storage()->NotifyDoneInstallingRegistration(
        this, version.get(), status);
    callback.Run(status);
}

} // namespace content
